#    This file is part of Metasm, the Ruby assembly manipulation suite
#    Copyright (C) 2006-2009 Yoann GUILLOT
#
#    Licence is LGPL, see LICENCE in the top-level directory


require 'Qt4'

module Metasm
module Gui

module Protect
  @@lasterror = Time.now
  def protect
    yield
  rescue Object
    puts $!.message, $!.backtrace	# also dump on stdout, for c/c
    delay = Time.now-@@lasterror
    sleep 1-delay if delay < 1	# msgbox flood protection
    @@lasterror = Time.now
    messagebox([$!.message, $!.backtrace].join("\n"), $!.class.name)
  end
end

module Msgbox
  include Protect

  # shows a message box (non-modal)
  # args: message, title/optionhash
  def messagebox(text, opts={})
    opts = {:title => opts} if opts.kind_of? String
    mbox = Qt::MessageBox.new#(self)
    mbox.text = text
    mbox.window_title = opts[:title] if opts[:title]
    mbox.window_modality = Qt::NonModal
    mbox.show
    mbox
  end

  # asks for user input, yields the result (unless 'cancel' clicked)
  # args: prompt, :text => default text, :title => title
  def inputbox(prompt, opts={})
    ibox = Qt::InputDialog.new#(self)
    ibox.label_text = prompt
    ibox.window_title = opts[:title] if opts[:title]
    ibox.text_value = opts[:text] if opts[:text]
    connect(ibox, SIGNAL('TextValueSelected(v)')) { |v| protect { yield v } }
    ibox.show
    ibox
  end

  @@dialogfilefolder = nil

  # asks to chose a file to open, yields filename
  # args: title, :path => path
  def openfile(title, opts={})
    f = Qt::FileDialog.get_open_file_name(nil, title, @@dialogfilefolder)
    if f and f != ''
      @@dialogfilefolder = File.dirname(f)
      protect { yield f }
    end
    f	# useless, dialog is modal
  end

  # same as openfile, but for writing a (new) file
  def savefile(title, opts={})
    f = Qt::FileDialog.get_save_file_name(nil, title, @@dialogfilefolder)
    if f and f != ''
      @@dialogfilefolder = File.dirname(f)
      protect { yield f }
    end
    f	# useless, dialog is modal
  end

  # shows a window with a list of items
  # the list is an array of arrays, displayed as String
  # the first array is the column names
  # each item clicked yields the block with the selected iterator, double-click also close the popup
  # args: title, [[col0 title, col1 title...], [col0 val0, col1 val0...], [val1], [val2]...]
  def listwindow(title, list, h={})
    l = Qt::TreeWidget.new#(self)
    l.window_title = title

    cols = list.shift
    #l.column_count = cols.length
    l.header_labels = cols
    list.each { |e|
      i = Qt::TreeWidgetItem.new
      e.length.times { |idx| i.set_text(idx, e[idx].to_s) }
      l.add_top_level_item i
    }

    connect(l, SIGNAL('itemActivated(QTreeWidgetItem*,int)')) { |item, col|
      next if not item.is_selected
      next if not idx = l.index_of_top_level_item(item)
      protect { yield(list[idx].map { |e| e.to_s }) } #if iter = treeview.selection.selected
    }
    connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*,int)')) { l.close }
    l.resize(cols.length*120, 400)
    l.show if not h[:noshow]
    l
  end
end

# a widget that holds many other widgets, and displays only one of them at a time
class ContainerChoiceWidget < Qt::StackedWidget
  include Msgbox

  attr_accessor :views, :view_indexes
  def initialize(*a)
    super()
    @views = {}
    @view_indexes = []

    initialize_widget(*a)
    initialize_visible if respond_to? :initialize_visible
  end

  def view(i)
    @views[i]
  end

  def showview(i)
    set_current_index @view_indexes.index(i)
  end

  def addview(name, w)
    @view_indexes << name
    @views[name] = w
    add_widget w
  end

  def curview
    @views[curview_index]
  end

  def curview_index
    @view_indexes[current_index]
  end
end

class ContainerVBoxWidget < Qt::VBoxLayout
  def initialize(*a)
    super()

    signal_connect('realize') { initialize_visible } if respond_to? :initialize_visible

    signal_connect('size_request') { |w, alloc| resize(*alloc) } if respond_to? :resize

    self.spacing = 2

    initialize_widget(*a)
  end
end

class DrawableWidget < Qt::Widget
  include Msgbox

  attr_accessor :parent_widget, :caret_x, :caret_y, :hl_word
  # this hash is used to determine the colors of the GUI elements (background, caret, ...)
  # modifications to it are only useful before the widget is first rendered (IE before GUI.main)
  attr_accessor :default_color_association

  # keypress event keyval traduction table
  # RHA no way to enumerate all Key_* constants, they are handled in Qt.const_missing
  Keyboard_trad = %w[
Escape Tab Backtab Backspace Return Enter Insert Delete Pause Print SysReq Clear Home End Left Up Right
Down PageUp PageDown Shift Control Meta Alt AltGr CapsLock NumLock ScrollLock
F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 F13 F14 F15 F16 F17 F18 F19 F20 F21 F22 F23 F24 F25 F26 F27 F28 F29 F30 F31 F32 F33 F34 F35 Super_L Super_R
Menu Hyper_L Hyper_R Help Direction_L Direction_R nobreakspace exclamdown cent sterling currency yen brokenbar section diaeresis copyright
ordfeminine guillemotleft notsign hyphen registered macron degree plusminus twosuperior threesuperior acute mu paragraph periodcentered cedilla onesuperior
masculine guillemotright onequarter onehalf threequarters questiondown Agrave Aacute Acircumflex Atilde Adiaeresis Aring AE Ccedilla Egrave Eacute
Ecircumflex Ediaeresis Igrave Iacute Icircumflex Idiaeresis ETH Ntilde Ograve Oacute Ocircumflex Otilde Odiaeresis multiply Ooblique Ugrave
Uacute Ucircumflex Udiaeresis Yacute THORN ssharp division ydiaeresis Multi_key Codeinput SingleCandidate MultipleCandidate PreviousCandidate Mode_switch Kanji Muhenkan
Henkan Romaji Hiragana Katakana Hiragana_Katakana Zenkaku Hankaku Zenkaku_Hankaku Touroku Massyo Kana_Lock Kana_Shift Eisu_Shift Eisu_toggle Hangul Hangul_Start
Hangul_End Hangul_Hanja Hangul_Jamo Hangul_Romaja Hangul_Jeonja Hangul_Banja Hangul_PreHanja Hangul_PostHanja Hangul_Special Dead_Grave Dead_Acute Dead_Circumflex Dead_Tilde Dead_Macron Dead_Breve Dead_Abovedot
Dead_Diaeresis Dead_Abovering Dead_Doubleacute Dead_Caron Dead_Cedilla Dead_Ogonek Dead_Iota Dead_Voiced_Sound Dead_Semivoiced_Sound Dead_Belowdot Dead_Hook Dead_Horn Back Forward Stop Refresh
VolumeDown VolumeMute VolumeUp BassBoost BassUp BassDown TrebleUp TrebleDown MediaPlay MediaStop MediaPrevious MediaNext MediaRecord HomePage Favorites Search
Standby OpenUrl LaunchMail LaunchMedia Launch0 Launch1 Launch2 Launch3 Launch4 Launch5 Launch6 Launch7 Launch8 Launch9 LaunchA LaunchB
LaunchC LaunchD LaunchE LaunchF MediaLast unknown Call Context1 Context2 Context3 Context4 Flip Hangup No Select Yes
Execute Printer Play Sleep Zoom Cancel
  ].inject({}) { |h, cst|
    v = Qt.const_get("Key_#{cst}").to_i	# AONETUHANOTEUHATNOHEU Qt::Enum != Fixnum
    key = cst.downcase.to_sym
    key = { :pageup => :pgup, :pagedown => :pgdown, :escape => :esc, :return => :enter }.fetch(key, key)
    h.update v => key
  }

  def initialize(*a)
    @parent_widget = nil

    @caret_x = @caret_y = 0		# text cursor position
    @oldcaret_x = @oldcaret_y = 1
    @hl_word = nil

    #@layout = Pango::Layout.new Gdk::Pango.context	# text rendering

    @color = {}
    @default_color_association = {:background => :palegrey}

    if a.last.kind_of? Qt::Widget
      super(a.last)
    else
      super()
    end

    { :white => 'fff', :palegrey => 'ddd', :black => '000', :grey => '444',
      :red => 'f00', :darkred => '800', :palered => 'fcc',
      :green => '0f0', :darkgreen => '080', :palegreen => 'cfc',
      :blue => '00f', :darkblue => '008', :paleblue => 'ccf',
      :yellow => 'ff0', :darkyellow => '440', :paleyellow => 'ffc',
    }.each { |tag, val| @color[tag] = color(val) }

    initialize_widget(*a)
    set_auto_fill_background true
    set_color_association @default_color_association
    set_focus_policy Qt::StrongFocus

    initialize_visible if respond_to? :initialize_visible

    set_font 'courier 10'
  end

  def keyPressEvent(key)
    val = key.key >= 128 ? Keyboard_trad[key.key] : key.text[0].ord	# must use text[0] to differenciate downcase/upcase
    if key.modifiers.to_i & Qt::ControlModifier.to_i > 0	# AONETHUAAAAAAAAAAAAAAA
      protect { keypress_ctrl(val) } if respond_to? :keypress_ctrl
    else
      protect { keypress(val) } if respond_to? :keypress
    end
  end

  def mousePressEvent(ev)
    if ev.modifiers.to_i & Qt::ControlModifier.to_i > 0
      protect { click_ctrl(ev.x, ev.y) } if respond_to? :click_ctrl
    else
      if ev.button == Qt::LeftButton
        protect { click(ev.x, ev.y) }
      elsif ev.button == Qt::RightButton
        protect { rightclick(ev.x, ev.y) } if respond_to? :rightclick
      end
    end
  end

  def mouseDoubleClickEvent(ev)
    protect { doubleclick(ev.x, ev.y) } if respond_to? :doubleclick
  end

  def mouseReleaseEvent(ev)
    if ev.button == Qt::LeftButton
      protect { mouserelease(ev.x, ev.y) } if respond_to? :mouserelease
    end
  end

  def mouseMoveEvent(ev)
    if ev.modifiers.to_i & Qt::ControlModifier.to_i > 0
      protect { mousemove_ctrl(ev.x, ev.y) } if respond_to? :mousemove_ctrl
    else
      protect { mousemove(ev.x, ev.y) } if respond_to? :mousemove
    end
  end

  def wheelEvent(ev)
    dir = ev.delta > 0 ? :up : :down
    if ev.modifiers.to_i & Qt::ControlModifier.to_i > 0
      protect { mouse_wheel_ctrl(dir, ev.x, ev.y) } if respond_to? :mouse_wheel_ctrl
    else
      protect { mouse_wheel(dir, ev.x, ev.y) } if respond_to? :mouse_wheel
    end
  end

  def resizeEvent(ev)
    protect { resized(ev.size.width, ev.size.height) } if respond_to? :resized
  end

  def grab_focus; set_focus end

  def paintEvent(*a)
    @painter = Qt::Painter.new(self)
    protect { paint }
    @painter.end
    @painter = nil
  end

  def paint
  end

  def gui_update
    redraw
  end

  # create a color from a 'rgb' description
  def color(val)
    @color[val] ||= Qt::Color.new(*val.unpack('CCC').map { |c| (c.chr*2).hex })
  end

  def set_caret_from_click(x, y)
    @caret_x = (x-1).to_i / @font_width
    @caret_y = y.to_i / @font_height
    update_caret
  end

  # change the font of the widget
  # arg is a Gtk Fontdescription string (eg 'courier 10')
  def set_font(descr)
    descr, sz = descr.split
    super(Qt::Font.new(descr, sz.to_i))
    @font_width = font_metrics.width('x')
    @font_height = font_metrics.line_spacing
    @font_descent = font_metrics.descent
    gui_update
  end

  # change the color association
  # arg is a hash function symbol => color symbol
  # color must be allocated
  # check #initialize/sig('realize') for initial function/color list
  def set_color_association(hash)
    hash.each { |k, v| @color[k] = color(v) }
    #set_background_role Qt::Palette::Window(color(:background))
    palette.set_color(Qt::Palette::Window, color(:background))
    gui_update
  end

  # update @hl_word from a line & offset, return nil if unchanged
  def update_hl_word(line, offset, mode=:asm)
    return if not line
    word = line[0...offset].to_s[/\w*$/] << line[offset..-1].to_s[/^\w*/]
    word = nil if word == ''
    if @hl_word != word
      if word
        if mode == :asm and defined?(@dasm) and @dasm
          re = @dasm.gui_hilight_word_regexp(word)
        else
          re = Regexp.escape word
        end
        @hl_word_re = /^(.*?)(\b(?:#{re})\b)/
      end
      @hl_word = word
    end
  end

  # invalidate the whole widget area
  def redraw
    invalidate(0, 0, 1000000, 1000000)
  end

  def invalidate_caret(cx, cy, x=0, y=0)
    invalidate(x + cx*@font_width, y + cy*@font_height, 2, @font_height)
  end

  def invalidate(x, y, w, h)
    update x, y, w, h
  end

  def resized(w, h)
    redraw
  end

  def keypress(key)
  end

  def keypress_ctrl(key)
  end

  def draw_color(col)
    @col = color(col)
    @painter.set_brush Qt::Brush.new(@col)
    @painter.set_pen Qt::Pen.new(@col)
  end

  def draw_rectangle(x, y, w, h)
    @painter.fill_rect(x, y, w, h, @col)
  end

  def draw_rectangle_color(col, x, y, w, h)
    draw_color(col)
    draw_rectangle(x, y, w, h)
  end

  def draw_line(x, y, ex, ey)
    @painter.draw_line(x, y, ex, ey)
  end

  def draw_line_color(col, x, y, ex, ey)
    draw_color(col)
    draw_line(x, y, ex, ey)
  end

  def draw_string(x, y, str)
    @painter.draw_text(x, y-@font_descent, str)
  end

  def draw_string_color(col, x, y, str)
    draw_color(col)
    draw_string(x, y, str)
  end
end

class Window < Qt::MainWindow
  include Msgbox

  attr_accessor :menu
  def initialize(*a)
    super()

    #connect(self, SIGNAL(:destroy)) { destroy_window }

    @menu = menu_bar

    screen = Qt::Application.desktop
    resize screen.width*3/4, screen.height*3/4

    initialize_window(*a)
    build_menu

    show
  end

  def build_menu
  end

  def destroy_window
  end

  def widget=(w)
    set_central_widget w
  end

  def title=(t); set_window_title(t) end
  def title; window_title end

  def new_menu
    Qt::Menu.new
  end

  def addsubmenu(menu, *args)
    accel = args.grep(/^\^?(\w|<\w+>)$/).first
    args.delete accel if accel
    check = args.delete :check
    submenu = args.grep(Qt::Menu).first
    args.delete submenu if submenu
    if label = args.shift
      label = label.capitalize if label == label.upcase	# TODO icon on OPEN/CLOSE etc
      label = label.gsub('_', '&')
    end

    if submenu
      submenu.title = label
      menu.add_menu submenu
      return
    end

    if check
      # TODO
      #item = Gtk::CheckMenuItem.new(label)
      #item.active = args.shift
      item = Qt::Action.new(label, self)
      menu.add_action item
    elsif label
      item = Qt::Action.new(label, self)
      menu.add_action item
    else
      menu.add_separator
    end
    item.setShortcut accel.sub('^', 'Ctrl+')

    connect(item, SIGNAL(:triggered)) { protect { yield(item) } } if block_given?

    item
  end
end

@app = Qt::Application.new ARGV

# start the GUI main loop
def self.main
  @app.exec
end

# ends the GUI main loop
def self.main_quit
  @app.quit	# XXX segfault..
end

# register a proc to be run whenever the gui loop is idle
# if the proc returns nil/false, delete it
def self.idle_add
  t = Qt::Timer.new
  t.connect(t, SIGNAL(:timeout)) { if not yield ; t.stop ; t = nil end }
  t.start
end

# run a single iteration of the main_loop
# e.g. call this from time to time when doing heavy computation, to keep the UI somewhat responsive
def self.main_iter
  Qt::Application.process_events
end

end
end

require 'metasm/gui/dasm_main'
require 'metasm/gui/debug'

